{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[TOC]\n",
    "\n",
    "<!-- File -->\n",
    "\n",
    "# 第十七章 文件\n",
    ">在丑陋的 Java I/O 编程方式诞生多年以后，Java终于简化了文件读写的基本操作。\n",
    "\n",
    "这种\"困难方式\"的全部细节都在 [Appendix: I/O Streams](./Appendix-IO-Streams.md)。如果你读过这个部分，就会认同 Java 设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来说是非常常用的，由于 I/O 糟糕的设计以至于\n",
    "很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。\n",
    "\n",
    "好像 Java 设计者终于意识到了 Java 使用者多年来的痛苦，在 Java7 中对此引入了巨大的改进。这些新元素被放在 **java.nio.file** 包下面，过去人们通常把 **nio** 中的 **n** 理解为 **new** 即新的 **io**，现在更应该当成是 **non-blocking** 非阻塞 **io**(**io**就是*input/output输入/输出*)。**java.nio.file** 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件：\n",
    "\n",
    "1. 文件或者目录的路径；\n",
    "2. 文件本身。\n",
    "\n",
    "<!-- File and Directory Paths -->\n",
    "## 文件和目录路径\n",
    "\n",
    "一个 **Path** 对象表示一个文件或者目录的路径，是一个跨操作系统（OS）和文件系统的抽象，目的是在构造路径时不必关注底层操作系统，代码可以在不进行修改的情况下运行在不同的操作系统上。**java.nio.file.Paths** 类包含一个重载方法 **static get()**，该方法接受一系列 **String** 字符串或一个*统一资源标识符*(URI)作为参数，并且进行转换返回一个 **Path** 对象："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/PathInfo.java\n",
    "import java.nio.file.*;\n",
    "import java.net.URI;\n",
    "import java.io.File;\n",
    "import java.io.IOException;\n",
    "\n",
    "public class PathInfo {\n",
    "    static void show(String id, Object p) {\n",
    "        System.out.println(id + \": \" + p);\n",
    "    }\n",
    "\n",
    "    static void info(Path p) {\n",
    "        show(\"toString\", p);\n",
    "        show(\"Exists\", Files.exists(p));\n",
    "        show(\"RegularFile\", Files.isRegularFile(p));\n",
    "        show(\"Directory\", Files.isDirectory(p));\n",
    "        show(\"Absolute\", p.isAbsolute());\n",
    "        show(\"FileName\", p.getFileName());\n",
    "        show(\"Parent\", p.getParent());\n",
    "        show(\"Root\", p.getRoot());\n",
    "        System.out.println(\"******************\");\n",
    "    }\n",
    "    public static void main(String[] args) {\n",
    "        System.out.println(System.getProperty(\"os.name\"));\n",
    "        info(Paths.get(\"C:\", \"path\", \"to\", \"nowhere\", \"NoFile.txt\"));\n",
    "        Path p = Paths.get(\"PathInfo.java\");\n",
    "        info(p);\n",
    "        Path ap = p.toAbsolutePath();\n",
    "        info(ap);\n",
    "        info(ap.getParent());\n",
    "        try {\n",
    "            info(p.toRealPath());\n",
    "        } catch(IOException e) {\n",
    "           System.out.println(e);\n",
    "        }\n",
    "        URI u = p.toUri();\n",
    "        System.out.println(\"URI: \" + u);\n",
    "        Path puri = Paths.get(u);\n",
    "        System.out.println(Files.exists(puri));\n",
    "        File f = ap.toFile(); // Don't be fooled\n",
    "    }\n",
    "}\n",
    "\n",
    "/* 输出:\n",
    "Windows 10\n",
    "toString: C:\\path\\to\\nowhere\\NoFile.txt\n",
    "Exists: false\n",
    "RegularFile: false\n",
    "Directory: false\n",
    "Absolute: true\n",
    "FileName: NoFile.txt\n",
    "Parent: C:\\path\\to\\nowhere\n",
    "Root: C:\\\n",
    "******************\n",
    "toString: PathInfo.java\n",
    "Exists: true\n",
    "RegularFile: true\n",
    "Directory: false\n",
    "Absolute: false\n",
    "FileName: PathInfo.java\n",
    "Parent: null\n",
    "Root: null\n",
    "******************\n",
    "toString: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\\PathInfo.java\n",
    "Exists: true\n",
    "RegularFile: true\n",
    "Directory: false\n",
    "Absolute: true\n",
    "FileName: PathInfo.java\n",
    "Parent: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "Root: C:\\\n",
    "******************\n",
    "toString: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "Exists: true\n",
    "RegularFile: false\n",
    "Directory: true\n",
    "Absolute: true\n",
    "FileName: files\n",
    "Parent: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\n",
    "Root: C:\\\n",
    "******************\n",
    "toString: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\\PathInfo.java\n",
    "Exists: true\n",
    "RegularFile: true\n",
    "Directory: false\n",
    "Absolute: true\n",
    "FileName: PathInfo.java\n",
    "Parent: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "Root: C:\\\n",
    "******************\n",
    "URI: file:///C:/Users/Bruce/Documents/GitHub/onjava/\n",
    "ExtractedExamples/files/PathInfo.java\n",
    "true\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我已经在这一章第一个程序的 **main()** 方法添加了第一行用于展示操作系统的名称，因此你可以看到不同操作系统之间存在哪些差异。理想情况下，差别会相对较小，并且使用 **/** 或者 **\\\\** 路径分隔符进行分隔。你可以看到我运行在Windows 10 上的程序输出。\n",
    "\n",
    "当 **toString()** 方法生成完整形式的路径，你可以看到 **getFileName()** 方法总是返回当前文件名。\n",
    "通过使用 **Files** 工具类(我们接下来将会更多地使用它)，可以测试一个文件是否存在，测试是否是一个\"普通\"文件还是一个目录等等。\"Nofile.txt\"这个示例展示我们描述的文件可能并不在指定的位置；这样可以允许你创建一个新的路径。\"PathInfo.java\"存在于当前目录中，最初它只是没有路径的文件名，但它仍然被检测为\"存在\"。一旦我们将其转换为绝对路径，我们将会得到一个从\"C:\"盘(因为我们是在Windows机器下进行测试)开始的完整路径，现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊，因为它取决于具体的文件系统。例如，如果文件名不区分大小写，即使路径由于大小写的缘故而不是完全相同，也可能得到肯定的匹配结果。在这样的平台上，**toRealPath()** 将返回实际情况下的 **Path**，并且还会删除任何冗余元素。\n",
    "\n",
    "这里你会看到 **URI** 看起来只能用于描述文件，实际上 **URI** 可以用于描述更多的东西；通过 [维基百科](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) 可以了解更多细节。现在我们成功地将 **URI** 转为一个 **Path** 对象。\n",
    "\n",
    "最后，你会在 **Path** 中看到一些有点欺骗的东西，这就是调用 **toFile()** 方法会生成一个 **File** 对象。听起来似乎可以得到一个类似文件的东西(毕竟被称为 **File** )，但是这个方法的存在仅仅是为了向后兼容。虽然看上去应该被称为\"路径\"，实际上却应该表示目录或者文件本身。这是个非常草率并且令人困惑的命名，但是由于 **java.nio.file** 的存在我们可以安全地忽略它的存在。\n",
    "\n",
    "### 选取路径部分片段\n",
    "**Path** 对象可以非常容易地生成路径的某一部分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/PartsOfPaths.java\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class PartsOfPaths {\n",
    "    public static void main(String[] args) {\n",
    "        System.out.println(System.getProperty(\"os.name\"));\n",
    "        Path p = Paths.get(\"PartsOfPaths.java\").toAbsolutePath();\n",
    "        for(int i = 0; i < p.getNameCount(); i++)\n",
    "            System.out.println(p.getName(i));\n",
    "        System.out.println(\"ends with '.java': \" +\n",
    "        p.endsWith(\".java\"));\n",
    "        for(Path pp : p) {\n",
    "            System.out.print(pp + \": \");\n",
    "            System.out.print(p.startsWith(pp) + \" : \");\n",
    "            System.out.println(p.endsWith(pp));\n",
    "        }\n",
    "        System.out.println(\"Starts with \" + p.getRoot() + \" \" + p.startsWith(p.getRoot()));\n",
    "    }\n",
    "}\n",
    "\n",
    "/* 输出:\n",
    "Windows 10\n",
    "Users\n",
    "Bruce\n",
    "Documents\n",
    "GitHub\n",
    "on-java\n",
    "ExtractedExamples\n",
    "files\n",
    "PartsOfPaths.java\n",
    "ends with '.java': false\n",
    "Users: false : false\n",
    "Bruce: false : false\n",
    "Documents: false : false\n",
    "GitHub: false : false\n",
    "on-java: false : false\n",
    "ExtractedExamples: false : false\n",
    "files: false : false\n",
    "PartsOfPaths.java: false : true\n",
    "Starts with C:\\ true\n",
    "*/\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以通过 **getName()** 来索引 **Path** 的各个部分，直到达到上限 **getNameCount()**。**Path** 也实现了 **Iterable** 接口，因此我们也可以通过增强的 for-each 进行遍历。请注意，即使路径以 **.java** 结尾，使用 **endsWith()** 方法也会返回 **false**。这是因为使用 **endsWith()** 比较的是整个路径部分，而不会包含文件路径的后缀。通过使用 **startsWith()** 和 **endsWith()** 也可以完成路径的遍历。但是我们可以看到，遍历 **Path** 对象并不包含根路径，只有使用 **startsWith()** 检测根路径时才会返回 **true**。\n",
    "\n",
    "### 路径分析\n",
    "**Files** 工具类包含一系列完整的方法用于获得 **Path** 相关的信息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/PathAnalysis.java\n",
    "import java.nio.file.*;\n",
    "import java.io.IOException;\n",
    "\n",
    "public class PathAnalysis {\n",
    "    static void say(String id, Object result) {\n",
    "        System.out.print(id + \": \");\n",
    "        System.out.println(result);\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) throws IOException {\n",
    "        System.out.println(System.getProperty(\"os.name\"));\n",
    "        Path p = Paths.get(\"PathAnalysis.java\").toAbsolutePath();\n",
    "        say(\"Exists\", Files.exists(p));\n",
    "        say(\"Directory\", Files.isDirectory(p));\n",
    "        say(\"Executable\", Files.isExecutable(p));\n",
    "        say(\"Readable\", Files.isReadable(p));\n",
    "        say(\"RegularFile\", Files.isRegularFile(p));\n",
    "        say(\"Writable\", Files.isWritable(p));\n",
    "        say(\"notExists\", Files.notExists(p));\n",
    "        say(\"Hidden\", Files.isHidden(p));\n",
    "        say(\"size\", Files.size(p));\n",
    "        say(\"FileStore\", Files.getFileStore(p));\n",
    "        say(\"LastModified: \", Files.getLastModifiedTime(p));\n",
    "        say(\"Owner\", Files.getOwner(p));\n",
    "        say(\"ContentType\", Files.probeContentType(p));\n",
    "        say(\"SymbolicLink\", Files.isSymbolicLink(p));\n",
    "        if(Files.isSymbolicLink(p))\n",
    "            say(\"SymbolicLink\", Files.readSymbolicLink(p));\n",
    "        if(FileSystems.getDefault().supportedFileAttributeViews().contains(\"posix\"))\n",
    "            say(\"PosixFilePermissions\",\n",
    "        Files.getPosixFilePermissions(p));\n",
    "    }\n",
    "}\n",
    "\n",
    "/* 输出:\n",
    "Windows 10\n",
    "Exists: true\n",
    "Directory: false\n",
    "Executable: true\n",
    "Readable: true\n",
    "RegularFile: true\n",
    "Writable: true\n",
    "notExists: false\n",
    "Hidden: false\n",
    "size: 1631\n",
    "FileStore: SSD (C:)\n",
    "LastModified: : 2017-05-09T12:07:00.428366Z\n",
    "Owner: MINDVIEWTOSHIBA\\Bruce (User)\n",
    "ContentType: null\n",
    "SymbolicLink: false\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在调用最后一个测试方法 **getPosixFilePermissions()** 之前我们需要确认一下当前文件系统是否支持 **Posix** 接口，否则会抛出运行时异常。\n",
    "\n",
    "### **Paths**的增减修改\n",
    "我们必须能通过对 **Path** 对象增加或者删除一部分来构造一个新的 **Path** 对象。我们使用 **relativize()** 移除 **Path** 的根路径，使用 **resolve()** 添加 **Path** 的尾路径(不一定是“可发现”的名称)。\n",
    "\n",
    "对于下面代码中的示例，我使用 **relativize()** 方法从所有的输出中移除根路径，部分原因是为了示范，部分原因是为了简化输出结果，这说明你可以使用该方法将绝对路径转为相对路径。\n",
    "这个版本的代码中包含 **id**，以便于跟踪输出结果："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/AddAndSubtractPaths.java\n",
    "import java.nio.file.*;\n",
    "import java.io.IOException;\n",
    "\n",
    "public class AddAndSubtractPaths {\n",
    "    static Path base = Paths.get(\"..\", \"..\", \"..\").toAbsolutePath().normalize();\n",
    "    \n",
    "    static void show(int id, Path result) {\n",
    "        if(result.isAbsolute())\n",
    "            System.out.println(\"(\" + id + \")r \" + base.relativize(result));\n",
    "        else\n",
    "            System.out.println(\"(\" + id + \") \" + result);\n",
    "        try {\n",
    "            System.out.println(\"RealPath: \" + result.toRealPath());\n",
    "        } catch(IOException e) {\n",
    "            System.out.println(e);\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) {\n",
    "        System.out.println(System.getProperty(\"os.name\"));\n",
    "        System.out.println(base);\n",
    "        Path p = Paths.get(\"AddAndSubtractPaths.java\").toAbsolutePath();\n",
    "        show(1, p);\n",
    "        Path convoluted = p.getParent().getParent()\n",
    "        .resolve(\"strings\").resolve(\"..\")\n",
    "        .resolve(p.getParent().getFileName());\n",
    "        show(2, convoluted);\n",
    "        show(3, convoluted.normalize());\n",
    "        Path p2 = Paths.get(\"..\", \"..\");\n",
    "        show(4, p2);\n",
    "        show(5, p2.normalize());\n",
    "        show(6, p2.toAbsolutePath().normalize());\n",
    "        Path p3 = Paths.get(\".\").toAbsolutePath();\n",
    "        Path p4 = p3.resolve(p2);\n",
    "        show(7, p4);\n",
    "        show(8, p4.normalize());\n",
    "        Path p5 = Paths.get(\"\").toAbsolutePath();\n",
    "        show(9, p5);\n",
    "        show(10, p5.resolveSibling(\"strings\"));\n",
    "        show(11, Paths.get(\"nonexistent\"));\n",
    "    }\n",
    "}\n",
    "\n",
    "/* 输出:\n",
    "Windows 10\n",
    "C:\\Users\\Bruce\\Documents\\GitHub\n",
    "(1)r onjava\\\n",
    "ExtractedExamples\\files\\AddAndSubtractPaths.java\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\\AddAndSubtractPaths.java\n",
    "(2)r on-java\\ExtractedExamples\\strings\\..\\files\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "(3)r on-java\\ExtractedExamples\\files\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "(4) ..\\..\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\on-java\n",
    "(5) ..\\..\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\on-java\n",
    "(6)r on-java\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\on-java\n",
    "(7)r on-java\\ExtractedExamples\\files\\.\\..\\..\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\on-java\n",
    "(8)r on-java\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\on-java\n",
    "(9)r on-java\\ExtractedExamples\\files\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\n",
    "(10)r on-java\\ExtractedExamples\\strings\n",
    "RealPath: C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\strings\n",
    "(11) nonexistent\n",
    "java.nio.file.NoSuchFileException:\n",
    "C:\\Users\\Bruce\\Documents\\GitHub\\onjava\\\n",
    "ExtractedExamples\\files\\nonexistent\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我还为 **toRealPath()** 添加了更多的测试，这是为了扩展和规则化，防止路径不存在时抛出运行时异常。\n",
    "\n",
    "<!-- Directories -->\n",
    "\n",
    "## 目录\n",
    "**Files** 工具类包含大部分我们需要的目录操作和文件操作方法。出于某种原因，它们没有包含删除目录树相关的方法，因此我们将实现并将其添加到 **onjava** 库中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// onjava/RmDir.java\n",
    "package onjava;\n",
    "\n",
    "import java.nio.file.*;\n",
    "import java.nio.file.attribute.BasicFileAttributes;\n",
    "import java.io.IOException;\n",
    "\n",
    "public class RmDir {\n",
    "    public static void rmdir(Path dir) throws IOException {\n",
    "        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {\n",
    "            @Override\n",
    "            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n",
    "                Files.delete(file);\n",
    "                return FileVisitResult.CONTINUE;\n",
    "            }\n",
    "            \n",
    "            @Override\n",
    "            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {\n",
    "                Files.delete(dir);\n",
    "                return FileVisitResult.CONTINUE;\n",
    "            }\n",
    "        });\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "删除目录树的方法实现依赖于 **Files.walkFileTree()**，\"walking\" 目录树意味着遍历每个子目录和文件。*Visitor* 设计模式提供了一种标准机制来访问集合中的每个对象，然后你需要提供在每个对象上执行的操作。\n",
    "此操作的定义取决于实现的 **FileVisitor** 的四个抽象方法，包括："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "1.  **preVisitDirectory()**：在访问目录中条目之前在目录上运行。 \n",
    "2.  **visitFile()**：运行目录中的每一个文件。  \n",
    "3.  **visitFileFailed()**：调用无法访问的文件。   \n",
    "4.  **postVisitDirectory()**：在访问目录中条目之后在目录上运行，包括所有的子目录。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了简化，**java.nio.file.SimpleFileVisitor** 提供了所有方法的默认实现。这样，在我们的匿名内部类中，我们只需要重写非标准行为的方法：**visitFile()** 和 **postVisitDirectory()** 实现删除文件和删除目录。两者都应该返回标志位决定是否继续访问(这样就可以继续访问，直到找到所需要的)。\n",
    "作为探索目录操作的一部分，现在我们可以有条件地删除已存在的目录。在以下例子中，**makeVariant()** 接受基本目录测试，并通过旋转部件列表生成不同的子目录路径。这些旋转与路径分隔符 **sep** 使用 **String.join()** 贴在一起，然后返回一个 **Path** 对象。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/Directories.java\n",
    "import java.util.*;\n",
    "import java.nio.file.*;\n",
    "import onjava.RmDir;\n",
    "\n",
    "public class Directories {\n",
    "    static Path test = Paths.get(\"test\");\n",
    "    static String sep = FileSystems.getDefault().getSeparator();\n",
    "    static List<String> parts = Arrays.asList(\"foo\", \"bar\", \"baz\", \"bag\");\n",
    "    \n",
    "    static Path makeVariant() {\n",
    "        Collections.rotate(parts, 1);\n",
    "        return Paths.get(\"test\", String.join(sep, parts));\n",
    "    }\n",
    "    \n",
    "    static void refreshTestDir() throws Exception {\n",
    "        if(Files.exists(test))\n",
    "        RmDir.rmdir(test);\n",
    "        if(!Files.exists(test))\n",
    "        Files.createDirectory(test);\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) throws Exception {\n",
    "        refreshTestDir();\n",
    "        Files.createFile(test.resolve(\"Hello.txt\"));\n",
    "        Path variant = makeVariant();\n",
    "        // Throws exception (too many levels):\n",
    "        try {\n",
    "            Files.createDirectory(variant);\n",
    "        } catch(Exception e) {\n",
    "            System.out.println(\"Nope, that doesn't work.\");\n",
    "        }\n",
    "        populateTestDir();\n",
    "        Path tempdir = Files.createTempDirectory(test, \"DIR_\");\n",
    "        Files.createTempFile(tempdir, \"pre\", \".non\");\n",
    "        Files.newDirectoryStream(test).forEach(System.out::println);\n",
    "        System.out.println(\"*********\");\n",
    "        Files.walk(test).forEach(System.out::println);\n",
    "    }\n",
    "    \n",
    "    static void populateTestDir() throws Exception  {\n",
    "        for(int i = 0; i < parts.size(); i++) {\n",
    "            Path variant = makeVariant();\n",
    "            if(!Files.exists(variant)) {\n",
    "                Files.createDirectories(variant);\n",
    "                Files.copy(Paths.get(\"Directories.java\"),\n",
    "                    variant.resolve(\"File.txt\"));\n",
    "                Files.createTempFile(variant, null, null);\n",
    "            }\n",
    "        }\n",
    "    }\n",
    "}\n",
    "\n",
    "/* 输出:\n",
    "Nope, that doesn't work.\n",
    "test\\bag\n",
    "test\\bar\n",
    "test\\baz\n",
    "test\\DIR_5142667942049986036\n",
    "test\\foo\n",
    "test\\Hello.txt\n",
    "*********\n",
    "test\n",
    "test\\bag\n",
    "test\\bag\\foo\n",
    "test\\bag\\foo\\bar\n",
    "test\\bag\\foo\\bar\\baz\n",
    "test\\bag\\foo\\bar\\baz\\8279660869874696036.tmp\n",
    "test\\bag\\foo\\bar\\baz\\File.txt\n",
    "test\\bar\n",
    "test\\bar\\baz\n",
    "test\\bar\\baz\\bag\n",
    "test\\bar\\baz\\bag\\foo\n",
    "test\\bar\\baz\\bag\\foo\\1274043134240426261.tmp\n",
    "test\\bar\\baz\\bag\\foo\\File.txt\n",
    "test\\baz\n",
    "test\\baz\\bag\n",
    "test\\baz\\bag\\foo\n",
    "test\\baz\\bag\\foo\\bar\n",
    "test\\baz\\bag\\foo\\bar\\6130572530014544105.tmp\n",
    "test\\baz\\bag\\foo\\bar\\File.txt\n",
    "test\\DIR_5142667942049986036\n",
    "test\\DIR_5142667942049986036\\pre7704286843227113253.non\n",
    "test\\foo\n",
    "test\\foo\\bar\n",
    "test\\foo\\bar\\baz\n",
    "test\\foo\\bar\\baz\\bag\n",
    "test\\foo\\bar\\baz\\bag\\5412864507741775436.tmp\n",
    "test\\foo\\bar\\baz\\bag\\File.txt\n",
    "test\\Hello.txt\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，**refreshTestDir()** 用于检测 **test** 目录是否已经存在。若存在，则使用我们新工具类 **rmdir()** 删除其整个目录。检查是否 **exists** 是多余的，但我想说明一点，因为如果你对于已经存在的目录调用 **createDirectory()** 将会抛出异常。**createFile()** 使用参数 **Path** 创建一个空文件; **resolve()** 将文件名添加到 **test Path** 的末尾。\n",
    "\n",
    "我们尝试使用 **createDirectory()** 来创建多级路径，但是这样会抛出异常，因为这个方法只能创建单级路径。我已经将 **populateTestDir()** 作为一个单独的方法，因为它将在后面的例子中被重用。对于每一个变量 **variant**，我们都能使用 **createDirectories()** 创建完整的目录路径，然后使用此文件的副本以不同的目标名称填充该终端目录。然后我们使用 **createTempFile()** 生成一个临时文件。\n",
    "\n",
    "在调用 **populateTestDir()** 之后，我们在 **test** 目录下面创建一个临时目录。请注意，**createTempDirectory()** 只有名称的前缀选项。与 **createTempFile()** 不同，我们再次使用它将临时文件放入新的临时目录中。你可以从输出中看到，如果未指定后缀，它将默认使用\".tmp\"作为后缀。\n",
    "\n",
    "为了展示结果，我们首次使用看起来很有希望的 **newDirectoryStream()**，但事实证明这个方法只是返回 **test** 目录内容的 Stream 流，并没有更多的内容。要获取目录树的全部内容的流，请使用 **Files.walk()**。\n",
    "\n",
    "<!-- File Systems -->\n",
    "\n",
    "## 文件系统\n",
    "为了完整起见，我们需要一种方法查找文件系统相关的其他信息。在这里，我们使用静态的 **FileSystems** 工具类获取\"默认\"的文件系统，但你同样也可以在 **Path** 对象上调用 **getFileSystem()** 以获取创建该 **Path** 的文件系统。你可以获得给定 *URI* 的文件系统，还可以构建新的文件系统(对于支持它的操作系统)。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/FileSystemDemo.java\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class FileSystemDemo {\n",
    "    static void show(String id, Object o) {\n",
    "        System.out.println(id + \": \" + o);\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) {\n",
    "        System.out.println(System.getProperty(\"os.name\"));\n",
    "        FileSystem fsys = FileSystems.getDefault();\n",
    "        for(FileStore fs : fsys.getFileStores())\n",
    "            show(\"File Store\", fs);\n",
    "        for(Path rd : fsys.getRootDirectories())\n",
    "            show(\"Root Directory\", rd);\n",
    "        show(\"Separator\", fsys.getSeparator());\n",
    "        show(\"UserPrincipalLookupService\",\n",
    "            fsys.getUserPrincipalLookupService());\n",
    "        show(\"isOpen\", fsys.isOpen());\n",
    "        show(\"isReadOnly\", fsys.isReadOnly());\n",
    "        show(\"FileSystemProvider\", fsys.provider());\n",
    "        show(\"File Attribute Views\",\n",
    "        fsys.supportedFileAttributeViews());\n",
    "    }\n",
    "}\n",
    "/* 输出:\n",
    "Windows 10\n",
    "File Store: SSD (C:)\n",
    "Root Directory: C:\\\n",
    "Root Directory: D:\\\n",
    "Separator: \\\n",
    "UserPrincipalLookupService:\n",
    "sun.nio.fs.WindowsFileSystem$LookupService$1@15db9742\n",
    "isOpen: true\n",
    "isReadOnly: false\n",
    "FileSystemProvider:\n",
    "sun.nio.fs.WindowsFileSystemProvider@6d06d69c\n",
    "File Attribute Views: [owner, dos, acl, basic, user]\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一个 **FileSystem** 对象也能生成 **WatchService** 和 **PathMatcher** 对象，将会在接下来两章中详细讲解。\n",
    "\n",
    "<!-- Watching a Path -->\n",
    "\n",
    "## 路径监听\n",
    "通过 **WatchService** 可以设置一个进程对目录中的更改做出响应。在这个例子中，**delTxtFiles()** 作为一个单独的任务执行，该任务将遍历整个目录并删除以 **.txt** 结尾的所有文件，**WatchService** 会对文件删除操作做出反应："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/PathWatcher.java\n",
    "// {ExcludeFromGradle}\n",
    "import java.io.IOException;\n",
    "import java.nio.file.*;\n",
    "import static java.nio.file.StandardWatchEventKinds.*;\n",
    "import java.util.concurrent.*;\n",
    "\n",
    "public class PathWatcher {\n",
    "    static Path test = Paths.get(\"test\");\n",
    "    \n",
    "    static void delTxtFiles() {\n",
    "        try {\n",
    "            Files.walk(test)\n",
    "            .filter(f ->\n",
    "                f.toString()\n",
    "                .endsWith(\".txt\"))\n",
    "                .forEach(f -> {\n",
    "                try {\n",
    "                    System.out.println(\"deleting \" + f);\n",
    "                    Files.delete(f);\n",
    "                } catch(IOException e) {\n",
    "                    throw new RuntimeException(e);\n",
    "                }\n",
    "            });\n",
    "        } catch(IOException e) {\n",
    "            throw new RuntimeException(e);\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) throws Exception {\n",
    "        Directories.refreshTestDir();\n",
    "        Directories.populateTestDir();\n",
    "        Files.createFile(test.resolve(\"Hello.txt\"));\n",
    "        WatchService watcher = FileSystems.getDefault().newWatchService();\n",
    "        test.register(watcher, ENTRY_DELETE);\n",
    "        Executors.newSingleThreadScheduledExecutor()\n",
    "        .schedule(PathWatcher::delTxtFiles,\n",
    "        250, TimeUnit.MILLISECONDS);\n",
    "        WatchKey key = watcher.take();\n",
    "        for(WatchEvent evt : key.pollEvents()) {\n",
    "            System.out.println(\"evt.context(): \" + evt.context() +\n",
    "            \"\\nevt.count(): \" + evt.count() +\n",
    "            \"\\nevt.kind(): \" + evt.kind());\n",
    "            System.exit(0);\n",
    "        }\n",
    "    }\n",
    "}\n",
    "/* Output:\n",
    "deleting test\\bag\\foo\\bar\\baz\\File.txt\n",
    "deleting test\\bar\\baz\\bag\\foo\\File.txt\n",
    "deleting test\\baz\\bag\\foo\\bar\\File.txt\n",
    "deleting test\\foo\\bar\\baz\\bag\\File.txt\n",
    "deleting test\\Hello.txt\n",
    "evt.context(): Hello.txt\n",
    "evt.count(): 1\n",
    "evt.kind(): ENTRY_DELETE\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**delTxtFiles()** 中的 **try** 代码块看起来有些多余，因为它们捕获的是同一种类型的异常，外部的 **try** 语句似乎已经足够了。然而出于某种原因，Java 要求两者都必须存在(这也可能是一个 bug)。还要注意的是在 **filter()** 中，我们必须显式地使用 **f.toString()** 转为字符串，否则我们调用 **endsWith()** 将会与整个 **Path** 对象进行比较，而不是路径名称字符串的一部分进行比较。\n",
    "\n",
    "一旦我们从 **FileSystem** 中得到了 **WatchService** 对象，我们将其注册到 **test** 路径以及我们感兴趣的项目的变量参数列表中，可以选择 **ENTRY_CREATE**，**ENTRY_DELETE** 或 **ENTRY_MODIFY**(其中创建和删除不属于修改)。\n",
    "\n",
    "因为接下来对 **watcher.take()** 的调用会在发生某些事情之前停止所有操作，所以我们希望 **deltxtfiles()** 能够并行运行以便生成我们感兴趣的事件。为了实现这个目的，我通过调用 **Executors.newSingleThreadScheduledExecutor()** 产生一个 **ScheduledExecutorService** 对象，然后调用 **schedule()** 方法传递所需函数的方法引用，并且设置在运行之前应该等待的时间。\n",
    "\n",
    "此时，**watcher.take()** 将等待并阻塞在这里。当目标事件发生时，会返回一个包含 **WatchEvent** 的 **Watchkey** 对象。展示的这三种方法是能对 **WatchEvent** 执行的全部操作。\n",
    "\n",
    "查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件，在 **Hello.txt** 被删除之前，**WatchService** 也不会被触发。你可能认为，如果说\"监视这个目录\"，自然会包含整个目录和下面子目录，但实际上：只会监视给定的目录，而不是下面的所有内容。如果需要监视整个树目录，必须在整个树的每个子目录上放置一个 **Watchservice**。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/TreeWatcher.java\n",
    "// {ExcludeFromGradle}\n",
    "import java.io.IOException;\n",
    "import java.nio.file.*;\n",
    "import static java.nio.file.StandardWatchEventKinds.*;\n",
    "import java.util.concurrent.*;\n",
    "\n",
    "public class TreeWatcher {\n",
    "\n",
    "    static void watchDir(Path dir) {\n",
    "        try {\n",
    "            WatchService watcher =\n",
    "            FileSystems.getDefault().newWatchService();\n",
    "            dir.register(watcher, ENTRY_DELETE);\n",
    "            Executors.newSingleThreadExecutor().submit(() -> {\n",
    "                try {\n",
    "                    WatchKey key = watcher.take();\n",
    "                    for(WatchEvent evt : key.pollEvents()) {\n",
    "                        System.out.println(\n",
    "                        \"evt.context(): \" + evt.context() +\n",
    "                        \"\\nevt.count(): \" + evt.count() +\n",
    "                        \"\\nevt.kind(): \" + evt.kind());\n",
    "                        System.exit(0);\n",
    "                    }\n",
    "                } catch(InterruptedException e) {\n",
    "                    return;\n",
    "                }\n",
    "            });\n",
    "        } catch(IOException e) {\n",
    "            throw new RuntimeException(e);\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    public static void main(String[] args) throws Exception {\n",
    "        Directories.refreshTestDir();\n",
    "        Directories.populateTestDir();\n",
    "        Files.walk(Paths.get(\"test\"))\n",
    "            .filter(Files::isDirectory)\n",
    "            .forEach(TreeWatcher::watchDir);\n",
    "        PathWatcher.delTxtFiles();\n",
    "    }\n",
    "}\n",
    "\n",
    "/* Output:\n",
    "deleting test\\bag\\foo\\bar\\baz\\File.txt\n",
    "deleting test\\bar\\baz\\bag\\foo\\File.txt\n",
    "evt.context(): File.txt\n",
    "evt.count(): 1\n",
    "evt.kind(): ENTRY_DELETE\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 **watchDir()** 方法中给 **WatchSevice** 提供参数 **ENTRY_DELETE**，并启动一个独立的线程来监视该**Watchservice**。这里我们没有使用 **schedule()** 进行启动，而是使用 **submit()** 启动线程。我们遍历整个目录树，并将 **watchDir()** 应用于每个子目录。现在，当我们运行 **deltxtfiles()** 时，其中一个 **Watchservice** 会检测到每一次文件删除。\n",
    "\n",
    "<!-- Finding Files -->\n",
    "\n",
    "## 文件查找\n",
    "到目前为止，为了找到文件，我们一直使用相当粗糙的方法，在 `path` 上调用 `toString()`，然后使用 `string` 操作查看结果。事实证明，`java.nio.file` 有更好的解决方案：通过在 `FileSystem` 对象上调用 `getPathMatcher()` 获得一个 `PathMatcher`，然后传入您感兴趣的模式。模式有两个选项：`glob` 和 `regex`。`glob` 比较简单，实际上功能非常强大，因此您可以使用 `glob` 解决许多问题。如果您的问题更复杂，可以使用 `regex`，这将在接下来的 `Strings` 一章中解释。\n",
    "\n",
    "在这里，我们使用 `glob` 查找以 `.tmp` 或 `.txt` 结尾的所有 `Path`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/Find.java\n",
    "// {ExcludeFromGradle}\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class Find {\n",
    "    public static void main(String[] args) throws Exception {\n",
    "        Path test = Paths.get(\"test\");\n",
    "        Directories.refreshTestDir();\n",
    "        Directories.populateTestDir();\n",
    "        // Creating a *directory*, not a file:\n",
    "        Files.createDirectory(test.resolve(\"dir.tmp\"));\n",
    "\n",
    "        PathMatcher matcher = FileSystems.getDefault()\n",
    "          .getPathMatcher(\"glob:**/*.{tmp,txt}\");\n",
    "        Files.walk(test)\n",
    "          .filter(matcher::matches)\n",
    "          .forEach(System.out::println);\n",
    "        System.out.println(\"***************\");\n",
    "\n",
    "        PathMatcher matcher2 = FileSystems.getDefault()\n",
    "          .getPathMatcher(\"glob:*.tmp\");\n",
    "        Files.walk(test)\n",
    "          .map(Path::getFileName)\n",
    "          .filter(matcher2::matches)\n",
    "          .forEach(System.out::println);\n",
    "        System.out.println(\"***************\");\n",
    "\n",
    "        Files.walk(test) // Only look for files\n",
    "          .filter(Files::isRegularFile)\n",
    "          .map(Path::getFileName)\n",
    "          .filter(matcher2::matches)\n",
    "          .forEach(System.out::println);\n",
    "    }\n",
    "}\n",
    "/* Output:\n",
    "test\\bag\\foo\\bar\\baz\\5208762845883213974.tmp\n",
    "test\\bag\\foo\\bar\\baz\\File.txt\n",
    "test\\bar\\baz\\bag\\foo\\7918367201207778677.tmp\n",
    "test\\bar\\baz\\bag\\foo\\File.txt\n",
    "test\\baz\\bag\\foo\\bar\\8016595521026696632.tmp\n",
    "test\\baz\\bag\\foo\\bar\\File.txt\n",
    "test\\dir.tmp\n",
    "test\\foo\\bar\\baz\\bag\\5832319279813617280.tmp\n",
    "test\\foo\\bar\\baz\\bag\\File.txt\n",
    "***************\n",
    "5208762845883213974.tmp\n",
    "7918367201207778677.tmp\n",
    "8016595521026696632.tmp\n",
    "dir.tmp\n",
    "5832319279813617280.tmp\n",
    "***************\n",
    "5208762845883213974.tmp\n",
    "7918367201207778677.tmp\n",
    "8016595521026696632.tmp\n",
    "5832319279813617280.tmp\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 `matcher` 中，`glob` 表达式开头的 `**/` 表示“当前目录及所有子目录”，这在当你不仅仅要匹配当前目录下特定结尾的 `Path` 时非常有用。单 `*` 表示“任何东西”，然后是一个点，然后大括号表示一系列的可能性---我们正在寻找以 `.tmp` 或 `.txt` 结尾的东西。您可以在 `getPathMatcher()` 文档中找到更多详细信息。\n",
    "\n",
    "`matcher2` 只使用 `*.tmp`，通常不匹配任何内容，但是添加 `map()` 操作会将完整路径减少到末尾的名称。\n",
    "\n",
    "注意，在这两种情况下，输出中都会出现 `dir.tmp`，即使它是一个目录而不是一个文件。要只查找文件，必须像在最后 `files.walk()` 中那样对其进行筛选。\n",
    "\n",
    "<!-- Reading & Writing Files -->\n",
    "## 文件读写\n",
    "此时，我们可以对路径和目录做任何事情。 现在让我们看一下操纵文件本身的内容。\n",
    "\n",
    "如果一个文件很“小”，也就是说“它运行得足够快且占用内存小”，那么 `java.nio.file.Files` 类中的实用程序将帮助你轻松读写文本和二进制文件。\n",
    "\n",
    "`Files.readAllLines()` 一次读取整个文件（因此，“小”文件很有必要），产生一个`List<String>`。 对于示例文件，我们将重用`streams/Cheese.dat`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/ListOfLines.java\n",
    "import java.util.*;\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class ListOfLines {\n",
    "    public static void main(String[] args) throws Exception {\n",
    "        Files.readAllLines(\n",
    "        Paths.get(\"../streams/Cheese.dat\"))\n",
    "        .stream()\n",
    "        .filter(line -> !line.startsWith(\"//\"))\n",
    "        .map(line ->\n",
    "            line.substring(0, line.length()/2))\n",
    "        .forEach(System.out::println);\n",
    "    }\n",
    "}\n",
    "/* Output:\n",
    "Not much of a cheese\n",
    "Finest in the\n",
    "And what leads you\n",
    "Well, it's\n",
    "It's certainly uncon\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "跳过注释行，其余的内容每行只打印一半。 这实现起来很简单：你只需将 `Path` 传递给 `readAllLines()` （以前的 java 实现这个功能很复杂）。`readAllLines()` 有一个重载版本，包含一个 `Charset` 参数来存储文件的 Unicode 编码。\n",
    "\n",
    "`Files.write()` 被重载以写入 `byte` 数组或任何 `Iterable` 对象（它也有 `Charset` 选项）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/Writing.java\n",
    "import java.util.*;\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class Writing {\n",
    "    static Random rand = new Random(47);\n",
    "    static final int SIZE = 1000;\n",
    "    \n",
    "    public static void main(String[] args) throws Exception {\n",
    "        // Write bytes to a file:\n",
    "        byte[] bytes = new byte[SIZE];\n",
    "        rand.nextBytes(bytes);\n",
    "        Files.write(Paths.get(\"bytes.dat\"), bytes);\n",
    "        System.out.println(\"bytes.dat: \" + Files.size(Paths.get(\"bytes.dat\")));\n",
    "\n",
    "        // Write an iterable to a file:\n",
    "        List<String> lines = Files.readAllLines(\n",
    "          Paths.get(\"../streams/Cheese.dat\"));\n",
    "        Files.write(Paths.get(\"Cheese.txt\"), lines);\n",
    "        System.out.println(\"Cheese.txt: \" + Files.size(Paths.get(\"Cheese.txt\")));\n",
    "    }\n",
    "}\n",
    "/* Output:\n",
    "bytes.dat: 1000\n",
    "Cheese.txt: 199\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们使用 `Random` 来创建一个随机的 `byte` 数组; 你可以看到生成的文件大小是 1000。\n",
    "\n",
    "一个 `List` 被写入文件，任何 `Iterable` 对象也可以这么做。\n",
    "\n",
    "如果文件大小有问题怎么办？ 比如说：\n",
    "\n",
    "1. 文件太大，如果你一次性读完整个文件，你可能会耗尽内存。\n",
    "\n",
    "2. 您只需要在文件的中途工作以获得所需的结果，因此读取整个文件会浪费时间。\n",
    "\n",
    "`Files.lines()` 方便地将文件转换为行的 `Stream`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/ReadLineStream.java\n",
    "import java.nio.file.*;\n",
    "\n",
    "public class ReadLineStream {\n",
    "    public static void main(String[] args) throws Exception {\n",
    "        Files.lines(Paths.get(\"PathInfo.java\"))\n",
    "          .skip(13)\n",
    "          .findFirst()\n",
    "          .ifPresent(System.out::println);\n",
    "    }\n",
    "}\n",
    "/* Output:\n",
    "    show(\"RegularFile\", Files.isRegularFile(p));\n",
    "*/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这对本章中第一个示例代码做了流式处理，跳过 13 行，然后选择下一行并将其打印出来。\n",
    "\n",
    "`Files.lines()` 对于把文件处理行的传入流时非常有用，但是如果你想在 `Stream` 中读取，处理或写入怎么办？这就需要稍微复杂的代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "attributes": {
     "classes": [
      "java"
     ],
     "id": ""
    }
   },
   "outputs": [],
   "source": [
    "// files/StreamInAndOut.java\n",
    "import java.io.*;\n",
    "import java.nio.file.*;\n",
    "import java.util.stream.*;\n",
    "\n",
    "public class StreamInAndOut {\n",
    "    public static void main(String[] args) {\n",
    "        try(\n",
    "          Stream<String> input =\n",
    "            Files.lines(Paths.get(\"StreamInAndOut.java\"));\n",
    "          PrintWriter output =\n",
    "            new PrintWriter(\"StreamInAndOut.txt\")\n",
    "        ) {\n",
    "            input.map(String::toUpperCase)\n",
    "              .forEachOrdered(output::println);\n",
    "        } catch(Exception e) {\n",
    "            throw new RuntimeException(e);\n",
    "        }\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为我们在同一个块中执行所有操作，所以这两个文件都可以在相同的 try-with-resources 语句中打开。`PrintWriter` 是一个旧式的 `java.io` 类，允许你“打印”到一个文件，所以它是这个应用的理想选择。如果你看一下 `StreamInAndOut.txt`，你会发现它里面的内容确实是大写的。\n",
    "\n",
    "<!-- Summary -->\n",
    "\n",
    "## 本章小结\n",
    "虽然本章对文件和目录操作做了相当全面的介绍，但是仍然有没被介绍的类库中的功能——一定要研究 `java.nio.file` 的 Javadocs，尤其是 `java.nio.file.Files` 这个类。\n",
    "\n",
    "Java 7 和 8 对于处理文件和目录的类库做了大量改进。如果您刚刚开始使用 Java，那么您很幸运。在过去，它令人非常不愉快，我确信 Java 设计者以前对于文件操作不够重视才没做简化。对于初学者来说这是一件很棒的事，对于教学者来说也一样。我不明白为什么花了这么长时间来解决这个明显的问题，但不管怎么说它被解决了，我很高兴。使用文件现在很简单，甚至很有趣，这是你以前永远想不到的。\n",
    "\n",
    "<!-- 分页 -->\n",
    "\n",
    "<div style=\"page-break-after: always;\"></div>"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 4
}
